package com.nulldev.util.web.YouTubeExtractor.channel;

import static com.nulldev.util.manage.Macros.unreachable;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.CLIENT_VERSION;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_STS;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getTabRendererWithContent;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getTextFromViewModel;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTStringF;

import java.io.FileNotFoundException;
import java.time.Duration;
import java.util.Collections;
import java.util.List;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.internal.backport.concurrency9.Maps;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.NonStandard;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.signatures.SignatureExtractor;
import com.nulldev.util.web.YouTubeExtractor.util.YTRequestObject;
import com.nulldev.util.web.YouTubeExtractor.util.YouTubeAmountFormatter;

public class ChannelExtractor {
	public static final String MAXIMUM_RESOLUTION_FLAG = "YTE_COMPAT_MAX_RES";
	public static final String MINIMUM_RESOLUTION_FLAG = "YTE_COMPAT_MIN_RES";

	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--debug", "-debug", "--trace", "--ChannelExtractor.debug"));
	private static final boolean ENABLE_NEW_PARSER = Arguments.notArguments("--ChannelExtractor.useLegacyParser");
	protected static final boolean USE_LEGACY_CONT_URLS = Arguments.hasArgument("--ChannelExtractor.legacyContURLs");
	protected static final boolean FORCE_DISABLE_NEW_CHANNEL_URLS = Arguments.hasArgument("--ChannelExtractor.disableNewChannelURLs");
	static final String YT_SHORTS_TIME_LIMIT = "-4"; // "1:00";

	private static final List<Video> EMPTY_VIDEO_LIST = Lists.of();
	private static final List<Thumbnail> EMPTY_AVATAR_LIST = Lists.of();
	private static final List<Thumbnail> EMPTY_BANNER_LIST = Lists.of();

	/*
	 * The legacy scraper has been removed as of March 2020, due to the non-Polymer
	 * UI being killed off.
	 * 
	 * Completed: - Add new parser.
	 * 
	 * TODO: - Remove the old parser. (delayed until 2.3)
	 * 
	 * OPTIONAL: - Rewrite the Channel API
	 */

	public static class Channel {
		private final List<Video> videos;
		private List<Album> albums;
		private final List<Thumbnail> banners;
		private final List<Thumbnail> avatars;

		private final String channelName;
		private String channelID;
		private long subscribers;
		private String description;
		private String continuation, continuationClickTracking;
		private boolean useYTIforCont;

		public Channel(final List<Video> videos, final List<Thumbnail> banners, final List<Thumbnail> avatars, final String channelName,
				final long subscribers) {
			this.videos = videos;
			this.channelName = channelName;
			this.avatars = avatars;
			this.banners = banners;
			this.subscribers = subscribers;
			this.description = "No description found. Huh?";
		}

		public List<Video> getVideos() {
			return Collections.unmodifiableList(videos);
		}

		public List<Thumbnail> getBanners() {
			return Collections.unmodifiableList(banners);
		}

		public List<Thumbnail> getAvatars() {
			return Collections.unmodifiableList(avatars);
		}

		public String getChannelName() {
			return this.channelName;
		}

		private boolean familyFriendly = true, paidContent, verifiedChannel;

		/**
		 * ????????? <br>
		 * This is a meta tag for youtube channels.
		 * 
		 * @returns boolean
		 */
		public boolean isFamilyFriendly() {
			return this.familyFriendly;
		}

		public boolean hasPaidContent() {
			return this.paidContent;
		}

		public String getChannelID() {
			return this.channelID;
		}

		public long getSubscriberCount() {
			return this.subscribers;
		}

		public String getDescription() {
			return this.description;
		}

		protected Channel setDescription(final String text) {
			this.description = text;
			return this;
		}

		public boolean isMusicChannel() {
			return this.albums != null && !this.albums.isEmpty();
		}

		public List<Album> getAlbums() {
			return this.albums;
		}

		protected Channel setAlbums(final List<Album> albums) {
			this.albums = albums;
			return this;
		}

		protected Channel setChannelId(final String channelID) {
			this.channelID = channelID;
			return this;
		}

		public boolean isVerified() {
			return this.verifiedChannel;
		}

		@NonStandard
		public void clear() {
			if (this.albums != null)
				this.albums.clear();
			if (this.videos != null)
				this.videos.clear();
		}

		protected Channel setContinuation(final String cont) {
			this.continuation = cont;
			return this;
		}

		public boolean hasContinuation() {
			return this.continuation != null && !this.continuation.isEmpty();
		}

		protected Channel setContClickTracking(final String ctp) {
			this.continuationClickTracking = ctp;
			return this;
		}

		public boolean hasContClickTracking() {
			return this.continuationClickTracking != null && !this.continuationClickTracking.isEmpty();
		}

		public String getContinuation() {
			return this.continuation;
		}

		public String getContClickTracking() {
			return this.continuationClickTracking;
		}

		public boolean useYTIforCont() {
			return this.useYTIforCont;
		}

		protected Channel setYTIforCont(final boolean use) {
			this.useYTIforCont = use;
			return this;
		}

		protected Channel setSubscriberCount(final long subCount) {
			this.subscribers = subCount;
			return this;
		}

		@Override
		public String toString() {
			return String.format("Channel[hash=%s,name=%s,hasContinuation=%b]", MathUtil.toHex(super.hashCode()), this.channelName, hasContinuation());
		}
	}

	public static class Video {
		private String title;
		private String url;
		private String thumbnail;
		private String videoID;
		private long duration;
		private String animatedThumbnailUrl;
		private long upcomingStartTime = -1L;

		public Video(final String title, final String videoID, final long duration) {
			this.title = title;
			this.url = "/watch?v=" + videoID;
			this.thumbnail = "https://i.ytimg.com/vi/" + videoID + "/mqdefault.jpg";
			this.videoID = videoID;
			this.duration = duration;
		}

		public String getTitle() {
			return this.title;
		}

		public String getURL() {
			return this.url;
		}

		public String getThumbURL() {
			return this.thumbnail;
		}

		public String getID() {
			return this.videoID;
		}

		public long duration() {
			return this.duration;
		}

		public boolean isLive() {
			return this.duration == -2;
		}

		public boolean isUpcoming() {
			return this.duration == -3;
		}

		public boolean isShorts() {
			return this.duration == -4;
		}

		public boolean hasUpcomingStartTime() {
			return this.upcomingStartTime != -1;
		}

		public long getUpcomingStartTime() {
			return this.upcomingStartTime;
		}

		public boolean hasAnimatedThumbnail() {
			return this.animatedThumbnailUrl != null;
		}

		protected Video setAnimatedThumbnail(final String url) {
			if (url != null && !url.isEmpty())
				this.animatedThumbnailUrl = url;
			return this;
		}

		protected Video setUpcomingStartTime(final long startTime) {
			if (startTime < -1L)
				return this;
			this.upcomingStartTime = startTime;
			return this;
		}

		public String getAnimatedThumbnailURL() {
			return this.animatedThumbnailUrl;
		}
	}

	public static class Album {
		private String title;
		private String url;
		private String thumbnail;
		private long videos;

		public Album(final String title, final String url, final String thumbnail, final long videos) {
			this.title = title;
			this.url = url;
			this.thumbnail = thumbnail;
			this.videos = videos;
		}

		public String getTitle() {
			return this.title;
		}

		public String getURL() {
			return this.url;
		}

		public String getThumbnail() {
			return this.thumbnail;
		}

		public long trackCount() {
			return this.videos;
		}
	}

	public static class Thumbnail {
		private final int width, height;
		private final String url;
		private final String flag;

		public Thumbnail(final int width, final int height, final String url) {
			this(width, height, url, null);
		}

		public Thumbnail(final int width, final int height, final String url, final String flag) {
			this.width = width;
			this.height = height;
			this.url = Variables.requireNonNullObject(url, "url");
			this.flag = /* __nullable__ */ flag;
		}

		public int width() {
			return this.width;
		}

		public int height() {
			return this.height;
		}

		public String url() {
			return this.url;
		}

		public String flag() {
			return this.flag;
		}

		public boolean hasFlag() {
			return this.flag != null;
		}
	}

	public static Channel extract(final String channelID) throws Exception {
		return extract(channelID, null, null);
	}

	public static Channel extract(final String channelID, final boolean useYTI) throws Exception {
		return extract(channelID, null, null, false);
	}

	public static Channel extract(String channelID, final String continuation, final String clickTrackingParams) throws Exception {
		return extract(channelID, continuation, clickTrackingParams, false);
	}

	public static Channel extract(String channelID, final String continuation, final String clickTrackingParams, final boolean useYTI) throws Exception {
		return extract(channelID, continuation, clickTrackingParams, useYTI, true);
	}

	public static Channel extract(String channelID, final String continuation, final String clickTrackingParams, final boolean useYTI,
			final boolean sendConsentCookie) throws Exception {
		final List<Video> videos = FastLists.list(20);
		final List<Album> albums = FastLists.list();
		try {
			if (ENABLE_NEW_PARSER) {
				String URL = "https://www.youtube.com/channel/" + channelID + "/videos?pbj=1";
				if (continuation != null && !continuation.isEmpty()) {
					if (USE_LEGACY_CONT_URLS)
						URL = "https://www.youtube.com/browse_ajax?ctoken=" + continuation + "&continuation=" + continuation;
					else
						URL = "https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false";
				} else {
					if (channelID.startsWith("/user/")) {
						channelID = channelID.substring(1);
						URL = "https://www.youtube.com/" + channelID + "/videos?pbj=1";
					} else if (channelID.startsWith("/") && !channelID.startsWith("/user/")) {
						URL = "https://www.youtube.com/user/" + channelID + "videos?pbj=1";
					}
				}
				final boolean isJSONRequest = !USE_LEGACY_CONT_URLS && continuation != null && !continuation.isEmpty();
				String jpl = null;
				if (isJSONRequest) {
					if (continuation != null && !continuation.isEmpty()) {
						final UniJsonValue o = YTRequestObject.makeYTRequestObject(clickTrackingParams != null ? FastMaps.fixedUniMap(new Object[][]
							{
									{ YTRequestObject.YT_REQUEST_CLICK_TRACKING_PARAMS, clickTrackingParams } })
								: Maps.of());
						o.add("continuation", continuation);
						final String op = o.toString();
						jpl = op;
					}
				}
				String _result = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(15))
						.headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES : YouTubeExtractor.BLANK_COOKIES).header("X-YouTube-Client-Name", "1")
						.header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-Youtube-Bootstrap-Logged-In", "false")
						.header("X-Goog-EOM-Visitor-Id", YouTubeExtractorConstants.VISITOR_DATA)
						.header(isJSONRequest ? new Entry("Content-Type", "application/json") : null)
						.create(isJSONRequest ? HttpMethod.POST : HttpMethod.GET, jpl).dataThenClose();
				if (_result.contains("\"apiUrl\":\"/youtubei/v1/browse\"") && !_result.contains("metadata") && !FORCE_DISABLE_NEW_CHANNEL_URLS
						&& !_result.contains("appendContinuationItemsAction")) {
					log.warn("YouTube requests using the YouTubeI API, re-asking for data.");
					final UniJsonValue s = UniJsonValue.parse(_result);
					final String browseId;
					if (s.isArray()) {
						browseId = s.last().get("endpoint").get("browseEndpoint").getString("browseId");
					} else {
						browseId = s.get("endpoint").get("browseEndpoint").getString("browseId");
					}
					if (browseId == null) {
						log.error("Invalid browseId! Bailing out.");
						log.error(s);
						return null;
					} else
						log.debug("Using browseId: " + browseId);
					final UniJsonValue o = YTRequestObject.makeYTRequestObject(Maps.of());
					o.add("browseId", browseId);
					final String op = o.toString();
					_result = HttpClient.getString()
							.url("https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false")
							.timeout(Duration.ofSeconds(15)).header("X-YouTube-Client-Name", "1").header("X-YouTube-Client-Version", CLIENT_VERSION)
							.header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL).header("X-YouTube-STS", PAGE_STS)
							.header("X-YouTube-Utc-Offset", UTC_OFFSET).header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM).header("Referer", URL)
							.header("Content-Type", "application/json").create(HttpMethod.POST, op).dataThenClose();
				}
				final String result = _result;
				final variable<Boolean> contUseYTI = new variable<Boolean>(false);
				if (result.startsWith("[") || result.endsWith("]")) {
					final UniJsonValue object = UniJsonValue.parseArray(result);
					if (object == null || object.isNull()) {
						log.error("Failed to parse JSON payload!");
						log.debug("Payload: " + result);
						return null;
					}
					final UniJsonValue pbo = object.arrayGetContains("response");
					if (pbo.getSafe("response").contains("continuationContents")) {
						final variable<String> cont = new variable<String>(null);
						final variable<String> ctp = new variable<String>(null);
						final UniJsonValue conts = pbo.get("response").get("continuationContents");
						if (conts.contains("gridContinuation")) {
							final UniJsonValue vids = conts.get("gridContinuation");
							if (vids != null && !vids.isNull() && vids.contains("items")) {
								vids.get("items").asArray().forEach((_video) -> {
									if (_video.hasValue("gridVideoRenderer")) {
										final UniJsonValue video = _video.get("gridVideoRenderer");
										String videoTitle = null;
										try {
											if (video.get("title").contains("runs")) {
												videoTitle = video.mget("title.runs").first().getString("text");
											} else {
												videoTitle = video.get("title").getString("simpleText");
											}
										} catch (Exception e) {
											log.error(e);
											video.println();
											videoTitle = "N/A";
										}
										if (videoTitle == null) {
											log.warn("videoTitle is null in: " + _video);
										}
										String time = "0:00";
										try {
											time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
													.get("thumbnailOverlayTimeStatusRenderer").get("text").getString("simpleText");
										} catch (Exception e) {
											log.error("Failed to get time. Data: " + video.toString(), e);
										}
										String animatedThumbnail = null;
										try {
											if (video.contains("richThumbnail")) {
												final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
												if (o.contains("movingThumbnailDetails")) {
													animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
												}
											}
										} catch (Exception e) {
											log.error(e);
										}
										long startTime = -1L;
										boolean isLive = false;
										boolean isUpcoming = false;
										try {
											if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style")
													.equalsIgnoreCase("LIVE"))
												isLive = true;
										} catch (Exception e) {
										}
										try {
											if (video.contains("upcomingEventData")) {
												isUpcoming = true;
												startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
											}
										} catch (Exception e) {
											log.error("Failed to check for upcoming video!", e);
										}
										try {
											videos.add(new Video(videoTitle, video.getString("videoId"),
													isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
													.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
										} catch (Exception e) {
											log.error("Failed to add video! [isLive: " + isLive + "]", e);
											log.debug(video);
										}
									} else if (_video.hasValue("continuationItemRenderer")) {
										final UniJsonValue cnt = _video.get("continuationItemRenderer");
										cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
										ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
										contUseYTI.set(true);
									} else {
										log.warn("[cont::items] Unknown video object!");
										log.warn(_video.toString());
									}
								});
								if (vids.hasValue("continuations")) {
									cont.set(vids.get("continuations").first().get("nextContinuationData").getString("continuation"));
								}
								return new Channel(videos, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "CHANNEL_CONTINUATION", -1).setAlbums(albums)
										.setChannelId(channelID).setContinuation(cont.get()).setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
							} else if (vids != null && !vids.isNull() && vids.contains("content")) {
								final UniJsonValue _c = vids.get("content");
								final UniJsonValue c = (_c.hasValue("horizontalListRenderer") ? _c.get("horizontalListRenderer") : _c.get("gridRenderer"));
								c.get("items").asArray().forEach((_video) -> {
									if (_video.hasValue("gridVideoRenderer")) {
										final UniJsonValue video = _video.get("gridVideoRenderer");
										if (!video.contains("title")) {
											log.warn("title missing from: " + video);
										}
										String videoTitle;
										if (video.get("title").contains("runs")) {
											videoTitle = video.mget("title.runs").first().getString("text");
										} else {
											videoTitle = video.get("title").getString("simpleText");
										}
										String time = "0:00";
										try {
											time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
													.get("thumbnailOverlayTimeStatusRenderer").get("text").getString("simpleText");
										} catch (Exception e) {
											log.error("Failed to get time. Data: " + video.toString(), e);
										}
										String animatedThumbnail = null;
										try {
											if (video.contains("richThumbnail")) {
												final UniJsonValue o = video.get("richThumbnail.movingThumbnailRenderer");
												if (o.contains("movingThumbnailDetails")) {
													animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
												}
											}
										} catch (Exception e) {
											log.error(e);
										}
										try {
											videos.add(new Video(videoTitle, video.getString("videoId"), MathUtil.TimeToSeconds.parseTime(time))
													.setAnimatedThumbnail(animatedThumbnail));
										} catch (Exception e) {
											log.error("Failed to add video!", e);
											log.debug(video);
										}
									} else if (_video.hasValue("continuationItemRenderer")) {
										final UniJsonValue cnt = _video.get("continuationItemRenderer");
										cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
										ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
										contUseYTI.set(true);
									} else {
										log.warn("[cont::grid] Unknown video object!");
										log.warn(_video.toString());
									}
								});
								if (vids.hasValue("continuations")) {
									cont.set(vids.get("continuations").first().get("nextContinuationData").getString("continuation"));
								}
								return new Channel(videos, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "CHANNEL_CONTINUATION", -1).setAlbums(albums)
										.setChannelId(channelID).setContinuation(cont.get()).setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
							} else {
								log.warn("vids == null!");
								log.warn(conts.toString());
								return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
							}
						} else {
							log.warn("Unknown object: " + conts);
							return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
						}
					}
					final UniJsonValue metadata = pbo.mget("response.metadata.channelMetadataRenderer");
					final UniJsonValue header = __get_header(pbo);

					if (header == null)
						return null;

					UniJsonValue _tabs = pbo.mget("response.contents.twoColumnBrowseResultsRenderer.tabs");
					final variable<String> cont = new variable<String>(null);
					final variable<String> ctp = new variable<String>(null);
					if (_tabs == null || _tabs.isNull()) {
						if (pbo.mget("response").hasValue("onResponseReceivedActions")) {
							final UniJsonValue appendAction = pbo.mget("response.onResponseReceivedActions").first();
							if (appendAction.contains("appendContinuationItemsAction")) {
								appendAction.get("appendContinuationItemsAction").get("continuationItems").asArray().forEach((_video) -> {
									if (_video.hasValue("gridVideoRenderer")) {
										final UniJsonValue video = _video.get("gridVideoRenderer");
										String videoTitle = null;
										try {
											if (video.get("title").contains("runs")) {
												videoTitle = video.mget("title.runs").first().getString("text");
											} else {
												videoTitle = video.get("title").getString("simpleText");
											}
										} catch (Exception e) {
											log.error(e);
											video.println();
											videoTitle = "N/A";
										}
										if (videoTitle == null) {
											log.warn("videoTitle is null in: " + _video);
										}
										String time = "0:00";
										try {
											time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
													.get("thumbnailOverlayTimeStatusRenderer").get("text").getString("simpleText");
										} catch (Exception e) {
											log.error("Failed to get time. Data: " + video.toString(), e);
										}
										String animatedThumbnail = null;
										try {
											if (video.contains("richThumbnail")) {
												final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
												if (o.contains("movingThumbnailDetails")) {
													animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
												}
											}
										} catch (Exception e) {
											log.error(e);
										}
										long startTime = -1L;
										boolean isLive = false;
										boolean isUpcoming = false;
										try {
											if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style")
													.equalsIgnoreCase("LIVE"))
												isLive = true;
										} catch (Exception e) {
										}
										try {
											if (video.contains("upcomingEventData")) {
												isUpcoming = true;
												startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
											}
										} catch (Exception e) {
											log.error("Failed to check for upcoming video!", e);
										}
										try {
											videos.add(new Video(videoTitle, video.getString("videoId"),
													isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
													.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
										} catch (Exception e) {
											log.error("Failed to add video! [isLive: " + isLive + "]", e);
											log.debug(video);
										}
									} else if (_video.hasValue("continuationItemRenderer")) {
										final UniJsonValue cnt = _video.get("continuationItemRenderer");
										cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
										ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
										contUseYTI.set(true);
									} else {
										log.warn("[onResponse::append] Unknown video object!");
										log.warn(_video.toString());
									}
								});
							} else {
								log.error("Failed to find a appendAction object!");
								log.error(appendAction);
								return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
							}
						} else {
							log.error("Failed to find a valid tabs object!");
							log.error(pbo);
							return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
						}
					} else {
						final UniJsonIterableValue tabs = _tabs.asArray();
						final UniJsonValue tabContent = getTabRendererWithContent(tabs);
						if (tabContent != null) {
							final UniJsonValue content = tabContent.get("tabRenderer").get("content");
							UniJsonValue vids = null;
							if (content.contains("sectionListRenderer")) {
								vids = content.get("sectionListRenderer").get("contents");
								if (vids.length() > 2 && vids.get(2).contains("itemSectionRenderer")) {
									vids = vids.get(2).get("itemSectionRenderer").get("contents").get(0);
								} else {
									vids = vids.get(0).get("itemSectionRenderer").get("contents").get(0);
								}
							} else if (content.contains("richGridRenderer")) {
								vids = content.get("richGridRenderer").get("contents");
							} else {
								unreachable("Unsupported tabRenderer contents!");
							}
							if (vids.hasValue("gridRenderer")) {
								vids = vids.get("gridRenderer");
							} else if (vids.hasValue("shelfRenderer")) {
								vids = vids.get("shelfRenderer"); // TODO: Handle shelves.
							}
							if (vids != null && !vids.isNull() && vids.contains("items")) {
								vids.get("items").asArray().forEach((_video) -> {
									if (_video.hasValue("gridVideoRenderer")) {
										final UniJsonValue video = _video.get("gridVideoRenderer");
										String videoTitle = null;
										try {
											if (video.get("title").contains("runs")) {
												videoTitle = video.mget("title.runs").first().getString("text");
											} else {
												videoTitle = video.get("title").getString("simpleText");
											}
										} catch (Exception e) {
											log.error(e);
											video.println();
											videoTitle = "N/A";
										}
										if (videoTitle == null) {
											log.warn("videoTitle is null in: " + _video);
										}
										String time = "0:00";
										try {
											time = getYTString(video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
													.get("thumbnailOverlayTimeStatusRenderer").get("text"));
										} catch (Exception e) {
											log.error("Failed to get time. Data: " + video.toString(), e);
										}
										if (time.equalsIgnoreCase("SHORTS")) {
											// We can't do anything, here just set it to the max time that YT shorts can
											// last.
											time = YT_SHORTS_TIME_LIMIT;
										}
										String animatedThumbnail = null;
										try {
											if (video.contains("richThumbnail")) {
												final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
												if (o.contains("movingThumbnailDetails")) {
													animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
												}
											}
										} catch (Exception e) {
											log.error(e);
										}
										long startTime = -1L;
										boolean isLive = false;
										boolean isUpcoming = false;
										try {
											if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style")
													.equalsIgnoreCase("LIVE"))
												isLive = true;
										} catch (Exception e) {
										}
										try {
											if (video.contains("upcomingEventData")) {
												isUpcoming = true;
												startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
											}
										} catch (Exception e) {
											log.error("Failed to check for upcoming video!", e);
										}
										try {
											videos.add(new Video(videoTitle, video.getString("videoId"),
													isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
													.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
										} catch (Exception e) {
											log.error("Failed to add video! [isLive: " + isLive + "]", e);
											log.debug(video);
										}
									} else if (_video.hasValue("continuationItemRenderer")) {
										final UniJsonValue cnt = _video.get("continuationItemRenderer");
										cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
										ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
										contUseYTI.set(true);
									} else {
										log.warn("[items::gridVideoRenderer] Unknown video object!");
										log.warn(_video.toString());
									}
								});
								if (vids.hasValue("continuations")) {
									cont.set(vids.get("continuations").first().get("nextContinuationData").getString("continuation"));
								}
							} else if (vids != null && !vids.isNull() && vids.contains("content")) {
								final UniJsonValue _c = vids.get("content");
								final UniJsonValue c = _c.getEither("horizontalListRenderer", "gridRenderer", "expandedShelfContentsRenderer");
								__do_album_entries(c, albums, videos, cont, ctp, contUseYTI);
							} else if (vids.isArray()) {
								for (final UniJsonValue uv : vids.asArray()) {
									__handle_column_obj(uv, videos, cont, ctp, contUseYTI);
								}
							} else {
								log.warn("vids == null!");
								log.warn(tabs.toString());
							}
						} else {
							final UniJsonValue pre_album = tabs.get(0).get("tabRenderer").get("content").get("sectionListRenderer").get("contents");
							if (pre_album.length() <= 1) {
								log.warn("Unexpected size found, assuming these are videos!");
								final UniJsonValue p0 = pre_album.get(0) //
										.get("itemSectionRenderer").get("contents").get(0);
								if (p0.contains("gridRenderer")) {
									final UniJsonIterableValue vids = p0.get("gridRenderer").get("items").asArray();
									vids.asArray().forEach((_video) -> {
										if (_video.hasValue("gridVideoRenderer")) {
											final UniJsonValue video = _video.get("gridVideoRenderer");
											if (!video.contains("title")) {
												log.warn("title missing from: " + video);
											}
											videos.add(new Video(video.get("title").getString("simpleText"), video.getString("videoId"), 0));
										} else if (_video.hasValue("continuationItemRenderer")) {
											final UniJsonValue cnt = _video.get("continuationItemRenderer");
											cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
											ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
											contUseYTI.set(true);
										} else if (_video.hasValue("gridVideoRenderer")) {
											final UniJsonValue video = _video.get("gridVideoRenderer");
											if (!video.contains("title")) {
												log.warn("title missing from: " + video);
											}
											videos.add(new Video(video.get("title").getString("simpleText"), video.getString("videoId"), 0));
										} else {
											log.warn("Unknown video object!");
											log.warn(_video.toString());
										}
									});
								} else {
									if (p0 == null || !p0.contains("shelfRenderer")) {
										log.error("Invalid p0 obj: " + p0);
										throw new RuntimeException("Invalid object found!");
									}
									final UniJsonValue content = p0.get("shelfRenderer").get("content");
									if (!content.contains("horizontalListRenderer")) {
										log.error("Invalid content obj: " + content);
										throw new RuntimeException("Invalid object found!");
									}
									final UniJsonValue vids = content.get("horizontalListRenderer");
									__do_album_entries(vids, albums, videos, cont, ctp, contUseYTI);
								}
							} else {
								try {
									final UniJsonValue pa = pre_album.get(0) //
											.get("itemSectionRenderer").get("contents").get(0).get("shelfRenderer").get("content");
									UniJsonIterableValue album = null;
									if (pa.contains("expandedShelfContentsRenderer")) {
										album = pa.get("expandedShelfContentsRenderer").get("items").asArray();
									} else if (pa.contains("horizontalListRenderer")) {
										album = pa.get("horizontalListRenderer").get("items").asArray();
									}
									if (album == null)
										throw new NullPointerException("No album holder found!");
									album.forEach((_album) -> {
										if (_album.hasValue("playlistRenderer")) {
											final UniJsonValue a = _album.get("playlistRenderer");
											albums.add(new Album(a.get("title").getString("simpleText"),
													a.get("navigationEndpoint").get("commandMetadata").get("webCommandMetadata").getString("url"),
													a.get("thumbnailRenderer").get("playlistCustomThumbnailRenderer").get("thumbnail").get("thumbnails").first()
															.getString("url"),
													a.getLong("videoCount")));
										} else if (_album.hasValue("gridPlaylistRenderer")) {
											final UniJsonValue a = _album.get("gridPlaylistRenderer");
											final String title_runs = a.get("title").get("runs").first().getString("text");
											final int videoCount = MathUtil
													.safeParseInt(StringsUtil.onlyNumbers(a.get("videoCountText").get("runs").first().getString("text")), -1);
											albums.add(new Album(title_runs,
													a.get("navigationEndpoint").get("commandMetadata").get("webCommandMetadata").getString("url"),
													a.get("thumbnailRenderer").get("playlistCustomThumbnailRenderer").get("thumbnail").get("thumbnails").first()
															.getString("url"),
													videoCount));
										} else if (_album.hasValue("gridVideoRenderer")) {
											final UniJsonValue video = _album.get("gridVideoRenderer");
											if (!video.contains("title")) {
												log.warn("title missing from: " + video);
											}
											videos.add(new Video(video.get("title").getString("simpleText"), video.getString("videoId"), 0));
										} else {
											log.warn("Unknown album object!");
											log.warn(_album.toString());
										}
									});
								} catch (Exception e) {
									log.error("Failed to extract albums!", e);
									return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
								}
							}
						}
					}
					final List<Thumbnail> banners = ChannelUtil.makeBanners(header);
					final List<Thumbnail> avatars = ChannelUtil.makeAvatars(header);
					long subCount = -1L;
					try {
						final String[] sp = getYTString(header.get("subscriberCountText")).replace('\u00a0', '\u0020').split(" ");
						final String inp = ArrayUtils.StringArrayUtils.addTo(sp, 2);
						subCount = YouTubeAmountFormatter.format(inp);
					} catch (Exception e) {
						log.error("Failed to get sub count!", e);
					}

					final String channelTitle;
					if (!StringsUtil.isEmpty(continuation)) {
						channelTitle = "CHANNEL_CONT";
					} else if (header.contains("title")) {
						if (header.get("title").isString()) {
							channelTitle = header.getString("title");
						} else {
							channelTitle = getTextFromViewModel(header.get("title"));
						}
					} else {
						final UniJsonValue pvm = header.get("pageHeaderViewModel");
						channelTitle = getTextFromViewModel(pvm.get("title"));
					}

					if (channelTitle == null) {
						log.error(header);
						throw new RuntimeException("Failed to get channel title!");
					}

					final String channelId;
					if (header != null && header.contains("channelId")) {
						channelId = header.getString("channelId");
					} else {
						log.warn("Failed to find channel ID...");
						log.dwarn(header);
						channelId = null;
					}

					return new Channel(videos, banners, avatars, channelTitle, subCount).setAlbums(albums)
							.setDescription(metadata == null ? "null" : metadata.getString("description")).setChannelId(channelId).setContinuation(cont.get())
							.setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
				} else {
					final UniJsonValue object = UniJsonValue.parse(result);
					if (object.hasValue("error")) {
						log.error("Failed to request channel! Bailing out.");
						log.error(object.get("error"));
						return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "YouTube returned an error.", -1);
					}
					final variable<String> cont = new variable<String>(null);
					final variable<String> ctp = new variable<String>(null);
					if (object.hasValue("onResponseReceivedActions")) {
						final UniJsonValue appendAction = object.get("onResponseReceivedActions").first();
						if (appendAction.contains("appendContinuationItemsAction")) {
							appendAction.get("appendContinuationItemsAction").get("continuationItems").asArray().forEach((_video) -> {
								if (_video.hasValue("gridVideoRenderer")) {
									final UniJsonValue video = _video.get("gridVideoRenderer");
									String videoTitle = null;
									try {
										if (video.get("title").contains("runs")) {
											videoTitle = video.mget("title.runs").first().getString("text");
										} else {
											videoTitle = video.get("title").getString("simpleText");
										}
									} catch (Exception e) {
										log.error(e);
										video.println();
										videoTitle = "N/A";
									}
									if (videoTitle == null) {
										log.warn("videoTitle is null in: " + _video);
									}
									String time = "0:00";
									try {
										time = getYTString(video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer")
												.get("thumbnailOverlayTimeStatusRenderer").get("text"));
									} catch (Exception e) {
										log.error("Failed to get time. Data: " + video.toString(), e);
									}
									if (time.equalsIgnoreCase("SHORTS")) {
										// We can't do anything, here just set it to the max time that YT shorts can
										// last.
										time = YT_SHORTS_TIME_LIMIT;
									}
									String animatedThumbnail = null;
									try {
										if (video.contains("richThumbnail")) {
											final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
											if (o.contains("movingThumbnailDetails")) {
												animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
											}
										}
									} catch (Exception e) {
										log.error(e);
									}
									long startTime = -1L;
									boolean isLive = false;
									boolean isUpcoming = false;
									try {
										if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style")
												.equalsIgnoreCase("LIVE"))
											isLive = true;
									} catch (Exception e) {
									}
									try {
										if (video.contains("upcomingEventData")) {
											isUpcoming = true;
											startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
										}
									} catch (Exception e) {
										log.error("Failed to check for upcoming video!", e);
									}
									try {
										videos.add(new Video(videoTitle, video.getString("videoId"),
												isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time)).setAnimatedThumbnail(animatedThumbnail)
												.setUpcomingStartTime(startTime));
									} catch (Exception e) {
										log.error("Failed to add video! [isLive: " + isLive + "]", e);
										log.debug(video);
									}
								} else if (_video.hasValue("continuationItemRenderer")) {
									final UniJsonValue cnt = _video.get("continuationItemRenderer");
									cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
									ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
									contUseYTI.set(true);
								} else if (_video.contains("richItemRenderer")) {
									__handle_column_obj(_video, videos, cont, ctp, contUseYTI);
								} else {
									log.warn("[append::conts] Unknown video object!");
									log.warn(_video.toString());
								}
							});
							return new Channel(videos, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "CHANNEL_CONT", -1).setAlbums(albums).setDescription("null")
									.setContinuation(cont.get()).setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
						} else {
							log.error("Failed to find a appendAction object!");
							log.error(appendAction);
							return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
						}
					} else {
						if (object == null || object.isNull()) {
							log.error("Unable to handle object: " + object);
							return null;
						}
						if (object.contains("continuationContents")) {
							final UniJsonValue conts = object.get("continuationContents");
							final UniJsonValue richGridCont = conts.get("richGridContinuation");
							final UniJsonIterableValue contents = richGridCont.get("contents").asArray();
							for (final UniJsonValue uv : contents)
								__handle_column_obj(uv, videos, cont, ctp, contUseYTI);
							return new Channel(videos, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "CHANNEL_CONT", -1).setAlbums(albums).setDescription("null")
									.setContinuation(cont.get()).setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
						} else {
							final UniJsonValue response = object.get("response");
							if (response == null || response.isNull() || !response.contains("contents")) {
								log.error("Unable to handle object, missing response: " + object);
								return null;
							}
							final UniJsonValue browseResults = response.get("contents").getEither("twoColumnBrowseResultsRenderer");
							final UniJsonIterableValue tabs = browseResults.get("tabs").asArray();
							final UniJsonValue videosTab = getTabRendererWithContent(tabs);
							if (videosTab == null || videosTab.isNull()) {
								log.error("Failed to read tabs, bailing out...");
								log.derror(response);
								return null;
							}
							final UniJsonValue tc = videosTab.get("tabRenderer")
									/* */
									.get("content");
							if (tc.contains("richGridRenderer")) {
								final UniJsonValue rgr = tc.get("richGridRenderer")
										/* */
										.get("contents");
								for (final UniJsonValue uv : rgr.asArray())
									__handle_column_obj(uv, videos, cont, ctp, contUseYTI);
							} else {
								final UniJsonValue isr = tc
										/* */
										.get("sectionListRenderer")
										/* */
										.get("contents")
										/* */
										.first()
										/* */
										.get("itemSectionRenderer");

								final UniJsonValue first = isr.get("contents").first();
								if (first.contains("shelfRenderer")) {
									final UniJsonValue shelfRenderer = first.get("shelfRenderer");
									final UniJsonValue content = shelfRenderer.get("content").getEither("horizontalListRenderer", "gridRenderer",
											"expandedShelfContentsRenderer");

									__do_album_entries(content, albums, videos, cont, ctp, contUseYTI);
								} else if (first.contains("gridRenderer")) {
									final UniJsonValue gridRenderer = first.get("gridRenderer");

									for (final UniJsonValue uv : gridRenderer.get("items").asArray())
										__handle_column_obj(uv, videos, cont, ctp, contUseYTI);
								} else {
									log.derror(isr);
									throw new UnsupportedOperationException("Unknown itemSectionRenderer!");
								}
							}

							final UniJsonValue metadata = object.mget("response.metadata.channelMetadataRenderer");
							final UniJsonValue header = __get_header(object);

							if (header == null)
								return null;

							final List<Thumbnail> banners = ChannelUtil.makeBanners(header);
							final List<Thumbnail> avatars = ChannelUtil.makeAvatars(header);
							long subCount = -1L;
							try {
								if (header.contains("subscriberCountText")) {
									final String[] sp = header.get("subscriberCountText").getString("simpleText").replace('\u00a0', '\u0020').split(" ");
									final String inp = ArrayUtils.StringArrayUtils.addTo(sp, 2);
									subCount = YouTubeAmountFormatter.format(inp);
								} else if (header.contains("pageHeaderViewModel")) {
									final UniJsonValue pageHeaderViewModel = header.get("pageHeaderViewModel");
									final UniJsonValue parts = pageHeaderViewModel.get("metadata").get("contentMetadataViewModel").get("metadataRows")
											.asArray();
									final UniJsonValue last = parts.last();
									final UniJsonValue metadataParts = last.get("metadataParts").asArray();
									if (metadataParts.size() == 2) {
										final UniJsonValue subs = metadataParts.first();
										final String text = subs.get("text").getString("content");

										final String[] sp = text.replace('\u00a0', '\u0020').split(" ");
										final String inp = ArrayUtils.StringArrayUtils.addTo(sp, 2);
										subCount = YouTubeAmountFormatter.format(inp);
									} else {
										log.warn("[subCount] Unknown metadataParts: " + metadataParts);
									}
								} else {
									log.warn("Cannot get subCount! (UNKNOWN_HEADER_TYPE)");
								}
							} catch (Exception e) {
								log.error("Failed to get sub count!", e);
							}
							final String channelTitle;
							if (!StringsUtil.isEmpty(continuation)) {
								channelTitle = "CHANNEL_CONT";
							} else if (header.contains("title")) {
								if (header.get("title").isString()) {
									channelTitle = header.getString("title");
								} else {
									channelTitle = getTextFromViewModel(header.get("title"));
								}
							} else {
								final UniJsonValue pvm = header.get("pageHeaderViewModel");
								channelTitle = getTextFromViewModel(pvm.get("title"));
							}

							if (channelTitle == null) {
								log.error(header);
								throw new RuntimeException("Failed to get channel title!");
							}

							final String channelId;
							if (header != null && header.contains("channelId")) {
								channelId = header.getString("channelId");
							} else {
								log.warn("Failed to find channel ID...");
								log.dwarn(header);
								channelId = null;
							}

							return new Channel(videos, banners, avatars, channelTitle, subCount).setAlbums(albums)
									.setDescription(metadata == null ? "null" : metadata.getString("description")).setChannelId(channelId)
									.setContinuation(cont.get()).setContClickTracking(ctp.get()).setYTIforCont(contUseYTI.get());
						}
					}
				}
			} else {
				return __do_legacy_path(channelID, continuation, videos, albums);
			}
		} catch (Exception e) {
			if (e instanceof FileNotFoundException || e instanceof NullPointerException) {
				log.error("Failed to extract channel!", e);
				return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
			}
			throw e;
		}
	}

	private static Channel __do_legacy_path(String channelID, final String continuation, final List<Video> videos, final List<Album> albums) throws Exception {
		if (continuation != null && !continuation.isEmpty()) {
			log.warn("The legacy API does not support continuations! Bailing out...");
			return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
		}
		String URL = "https://www.youtube.com/channel/" + channelID + "/videos";
		// boolean isUser = false;
		if (channelID.startsWith("/user/")) {
			channelID = channelID.substring(1);
			// isUser = true;
			URL = "https://www.youtube.com/" + channelID + "/videos";
		} else if (channelID.startsWith("/") && !channelID.startsWith("/user/")) {
			URL = "https://www.youtube.com/" + channelID + "/videos";
		}
		// final = (isUser ? "https://www.youtube.com/" + channelID +
		// "/videos?shelf_id=0&sort=dd&view=0"
		// : "https://www.youtube.com/channel/" + channelID +
		// "/videos?shelf_id=0&sort=dd&view=0");
		final String result = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(10)).create().dataThenClose();
		// final String toParse = "{" + StringsUtil.substringBetweenO(
		// result, "\"selected\":true,\"content\":{", "}}]}},") + "}";
		final Executable<UniJsonValue> ytInitialData = ExecutorManager.globalAsyncSubmit(() -> {
			try {
				final String ss = StringsUtil.substringBetweenO(result, "window[\"ytInitialData\"] = {", "};<");
				if (ss == null) {
					final String ss1 = StringsUtil.substringBetweenO(result, ">var ytInitialData = {", "};<");
					if (ss1 == null) {
						log.error("Failed to fetch ytInitialData!");
						log.debug(result);
						return null;
					}
					return UniJsonValue.parse("{" + ss1 + "}", true);
				}
				return UniJsonValue.parse("{" + ss + "}", true);
			} catch (Exception e) {
				log.error("Failed to parse ytInitialData!", e);
				return null;
			}
		});
		if (ytInitialData.waitTillCompletion() == null) {
			log.error("Failed to find initial data!");
			return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
		}
		final UniJsonValue object = ytInitialData.result();
		final UniJsonValue content = object.get("contents");
		if (content == null) {
			log.error("Failed to find content data!");
			log.debug(object);
			return new Channel(EMPTY_VIDEO_LIST, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "Channel not found.", -1);
		}
		try {
			UniJsonValue base = null;
			try {
				base = content.get("twoColumnBrowseResultsRenderer").get("tabs").asArray().get(1).get("tabRenderer").get("content").get("sectionListRenderer")
						.get("contents").asArray().get(0).get("itemSectionRenderer").get("contents").asArray().get(0).get("gridRenderer");
			} catch (Exception e) {
				base = content.get("twoColumnBrowseResultsRenderer").get("tabs").asArray().get(1).get("tabRenderer").get("content").get("sectionListRenderer")
						.get("contents").asArray().get(0).get("itemSectionRenderer").get("contents").asArray().get(0).get("gridRenderer");
			}
			final UniJsonValue vids = base.get("items").asArray();
			final UniJsonValue metadata = object.get("metadata").get("channelMetadataRenderer");
			final UniJsonValue header = object.get("header").get("c4TabbedHeaderRenderer");
			for (int i = 0; i < vids.length(); i++) {
				final UniJsonValue _vid = vids.get(i);
				if (!_vid.contains("gridVideoRenderer")) {
					log.warn("Unknown object: " + _vid);
					continue;
				}
				final UniJsonValue vid = _vid.get("gridVideoRenderer");
				final String title = vid.getSafe("title").contains("simpleText") ? vid.get("title").getString("simpleText")
						: vid.get("title").get("runs").first().getString("text");
				String time = "0:00";
				try {
					time = vid.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer").get("thumbnailOverlayTimeStatusRenderer")
							.get("text").getString("simpleText");
				} catch (Exception e) {
					log.error("Failed to get time. Data: " + vid, e);
				}
				String animatedThumbnail = null;
				try {
					if (vid.contains("richThumbnail")) {
						final UniJsonValue o = vid.mget("richThumbnail.movingThumbnailRenderer");
						if (o.contains("movingThumbnailDetails")) {
							animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
						}
					}
				} catch (Exception e) {
					log.error(e);
				}
				final String videoID = vid.getString("videoId");
				videos.add(new Video(title, videoID, MathUtil.TimeToSeconds.parseTime(time)).setAnimatedThumbnail(animatedThumbnail));
			}
			final List<Thumbnail> banners = ChannelUtil.makeBanners(header);
//			String rssFeed = null;
//			if (metadata.contains("rssFeed")) {
//				rssFeed = metadata.getString("rssUrl");
//			}
			long subscribers = -1;
			try {
				final String subscriber_base = StringsUtil.substringBetweenO(result, "\"subscriberCountText\":{\"simpleText\":\"", "\"");
				final String numberisied = StringsUtil.onlyNumbers(subscriber_base);
				subscribers = MathUtil.safeParseL(numberisied, -1);
				if (subscribers == -1) {
					log.warn("Failed to parse subscriber count. [got: " + subscriber_base + "]");
				}
			} catch (Exception e) {
				log.error("Failed to get subscriber count!", e);
			}
			return new Channel(videos, banners, EMPTY_AVATAR_LIST, metadata.getString("title"), subscribers).setDescription(metadata.getString("description"));
		} catch (Exception e) {
			try {
				// Probably a music channel.
				UniJsonValue base = null;
				try {
					base = content.get("twoColumnBrowseResultsRenderer").get("tabs").asArray().get(0).get("tabRenderer").get("content")
							.get("sectionListRenderer").get("contents").asArray().get(1).get("itemSectionRenderer").get("contents").asArray().get(0)
							.get("shelfRenderer").get("content").get("expandedShelfContentsRenderer");
				} catch (Exception exc) {
					base = content.get("twoColumnBrowseResultsRenderer").get("tabs").asArray().get(1).get("tabRenderer").get("content")
							.get("sectionListRenderer").get("contents").asArray().get(0).get("itemSectionRenderer").get("contents").asArray().get(0)
							.get("gridRenderer");
				}
				__do_album_entries(base, albums, videos, variable.makeLocked(null), variable.makeLocked(null), variable.makeLocked(false));
				return new Channel(videos, EMPTY_BANNER_LIST, EMPTY_AVATAR_LIST, "[PH] N/A", -1).setAlbums(albums);
			} catch (Exception e1) {
				throw e;
			}
		}
	}

	private static void __handle_column_obj(final UniJsonValue _video, final List<Video> videos, final variable<String> cont,
			final variable<String> contClickTrackingParam, final variable<Boolean> contUseYTI) {
		if (_video.hasValue("gridVideoRenderer")) {
			final UniJsonValue video = _video.get("gridVideoRenderer");
			String videoTitle = getYTStringF(video.get("title"), "N/A");
			String time = "0:00";
			try {
				time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer").get("thumbnailOverlayTimeStatusRenderer")
						.get("text").getString("simpleText");
			} catch (Exception e) {
				log.error("Failed to get time. Data: " + video.toString(), e);
			}
			String animatedThumbnail = null;
			try {
				if (video.contains("richThumbnail")) {
					final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
					if (o.contains("movingThumbnailDetails")) {
						animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
					}
				}
			} catch (Exception e) {
				log.error(e);
			}
			long startTime = -1L;
			boolean isLive = false;
			boolean isUpcoming = false;
			try {
				if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style").equalsIgnoreCase("LIVE"))
					isLive = true;
			} catch (Exception e) {
			}
			try {
				if (video.contains("upcomingEventData")) {
					isUpcoming = true;
					startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
				}
			} catch (Exception e) {
				log.error("Failed to check for upcoming video!", e);
			}
			try {
				videos.add(new Video(videoTitle, video.getString("videoId"), isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
						.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
			} catch (Exception e) {
				log.error("Failed to add video! [isLive: " + isLive + "]", e);
				log.debug(video);
			}
		} else if (_video.hasValue("richItemRenderer")) {
			final UniJsonValue video = _video.get("richItemRenderer").get("content").get("videoRenderer");
			final String videoTitle = getYTStringF(video.get("title"), "N/A");
			String time = "0:00";
			try {
				time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer").get("thumbnailOverlayTimeStatusRenderer")
						.get("text").getString("simpleText");
			} catch (Exception e) {
				log.error("Failed to get time. data: " + video.toString(), e);
			}
			String animatedThumbnail = null;
			try {
				if (video.contains("richThumbnail")) {
					final UniJsonValue o = video.mget("richThumbnail.movingThumbnailRenderer");
					if (o.contains("movingThumbnailDetails")) {
						animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
					}
				}
			} catch (Exception e) {
				log.error(e);
			}
			long startTime = -1L;
			boolean isLive = false;
			boolean isUpcoming = false;
			try {
				if (video.get("thumbnailOverlays").first().get("thumbnailOverlayTimeStatusRenderer").getString("style").equalsIgnoreCase("LIVE"))
					isLive = true;
			} catch (Exception e) {
			}
			try {
				if (video.contains("upcomingEventData")) {
					isUpcoming = true;
					startTime = MathUtil.safeParseL(video.get("upcomingEventData").getString("startTime", "-1"), -1L);
				}
			} catch (Exception e) {
				log.error("Failed to check for upcoming video!", e);
			}
			try {
				videos.add(new Video(videoTitle, video.getString("videoId"), isUpcoming ? -3 : isLive ? -2 : MathUtil.TimeToSeconds.parseTime(time))
						.setAnimatedThumbnail(animatedThumbnail).setUpcomingStartTime(startTime));
			} catch (Exception e) {
				log.error("Failed to add video! [isLive: " + isLive + "]", e);
				log.debug(video);
			}
		} else if (_video.hasValue("continuationItemRenderer")) {
			final UniJsonValue cnt = _video.get("continuationItemRenderer");
			cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
			contClickTrackingParam.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
			contUseYTI.set(true);
		} else {
			log.warn("[__handle_column_obj] Unknown video object!");
			log.warn(_video.toString());
		}
	}

	private static UniJsonValue __get_header(final UniJsonValue pbo) {
		final UniJsonValue _header = pbo.get("response")
				/* */
				.get("header");
		if (_header == null || _header.isNull()) {
			log.error("Failed to find header in response!");
			log.derror(pbo);
			return null;
		} else if (_header.contains("c4TabbedHeaderRenderer")) {
			return _header.get("c4TabbedHeaderRenderer");
		} else {
			final UniJsonValue phr = _header.get("pageHeaderRenderer");
			if (!phr.contains("content")) {
				log.error("Failed to find content in PAGE_HEADER_RENDERER!");
				phr.println();
				return null;
			}
			return phr
					/* */
					.get("content");
		}
	}

	private static void __do_album_entries(final UniJsonValue c, final List<Album> albums, final List<Video> videos, final variable<String> cont,
			final variable<String> ctp, final variable<Boolean> contUseYTI) {
		c.get("items").asArray().forEach((_video) -> {
			if (_video.hasValue("gridVideoRenderer")) {
				final UniJsonValue video = _video.get("gridVideoRenderer");
				if (!video.contains("title")) {
					log.warn("title missing from: " + video);
				}
				String videoTitle;
				if (video.get("title").contains("runs")) {
					videoTitle = video.mget("title.runs").first().getString("text");
				} else {
					videoTitle = video.get("title").getString("simpleText");
				}
				String time = "0:00";
				try {
					time = video.get("thumbnailOverlays").arrayGetContains("thumbnailOverlayTimeStatusRenderer").get("thumbnailOverlayTimeStatusRenderer")
							.get("text").getString("simpleText");
				} catch (Exception e) {
					log.error("Failed to get time. Data: " + video.toString(), e);
				}
				String animatedThumbnail = null;
				try {
					if (video.contains("richThumbnail")) {
						final UniJsonValue o = video.get("richThumbnail.movingThumbnailRenderer");
						if (o.contains("movingThumbnailDetails")) {
							animatedThumbnail = o.get("movingThumbnailDetails").get("thumbnails").last().getString("url");
						}
					}
				} catch (Exception e) {
					log.error(e);
				}
				videos.add(new Video(videoTitle, video.getString("videoId"), MathUtil.TimeToSeconds.parseTime(time)).setAnimatedThumbnail(animatedThumbnail));
			} else if (_video.hasValue("continuationItemRenderer")) {
				final UniJsonValue cnt = _video.get("continuationItemRenderer");
				cont.set(cnt.get("continuationEndpoint").get("continuationCommand").getString("token"));
				ctp.set(cnt.get("continuationEndpoint").getString("clickTrackingParams"));
				contUseYTI.set(true);
			} else if (_video.hasValue("gridPlaylistRenderer")) {
				final UniJsonValue video = _video.get("gridPlaylistRenderer");
				final String title = video.get("title").get("runs").first().getString("text");
				final UniJsonValue navigationEndpoint = video.get("navigationEndpoint");
				final UniJsonValue watchEndpoint = navigationEndpoint.get("watchEndpoint");
				final String vid = watchEndpoint.getString("videoId");
				final String url = "/watch?v=" + vid + "&list=" + watchEndpoint.getString("playlistId");
				String thumbnailUrl;
				try {
					final UniJsonValue thumbnailRenderer = video.get("thumbnailRenderer");
					if (thumbnailRenderer.contains("playlistCustomThumbnailRenderer")) {
						thumbnailUrl = thumbnailRenderer.get("playlistCustomThumbnailRenderer").get("thumbnail").get("thumbnails").last().getString("url");
					} else {
						thumbnailUrl = thumbnailRenderer.get("playlistVideoThumbnailRenderer").get("thumbnail").get("thumbnails").last().getString("url");
					}
				} catch (Exception ex) {
					log.warn("Failed to get playlistCustomThumbnailRenderer!");
					log.derror("thumbnailRenderer: " + video.get("thumbnailRenderer"));
					log.error(ex);
					thumbnailUrl = "https://i.ytimg.com/vi/" + vid + "/hqdefault.jpg";
				}
				final int videoCount = video.get("videoCountShortText").getInt("simpleText", 0);
				albums.add(new Album(title, url, thumbnailUrl, videoCount));
			} else {
				log.warn("[__do_album_entries] Unknown video object!");
				log.warn(_video.toString());
			}
		});
	}

	public static void main(String[] args) throws Exception {
		SignatureExtractor.autoVideoID("3AsRI_cSie0");
		final String channel = "UC_kRDKYrUlrbtrSiyu5Tflg";
		final Channel c = extract(channel);
		JVM.println(c);
		JVM.println(c != null ? c.getVideos() : "null");
		if (c.hasContinuation()) {
			JVM.println("cont: " + c.getContinuation());
			JVM.println("ctp: " + c.getContClickTracking());
			JVM.println(extract(channel, c.getContinuation(), c.getContClickTracking()));
		} else {
			log.warn("No continuation was found!");
		}
	}
}
